﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.IO;
using System.Windows.Forms;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using System.Text.RegularExpressions;
using ModelViewerAux;

namespace ModelViewer
{
    /// <summary>
    /// This class exports a model in the xmodl format. That's an XML format
    /// that's convenient for use with OpenGL.
    /// </summary>
    public class ModelExporter
    {
        /// <summary>
        /// .xmodl file format version we are using
        /// </summary>
        public static string Version { get { return "2.1"; } }

        private ViewerModel viewerModel = null;
        private Model model = null;
        private string fileName = "";
        private string filePath = "";
        private string fileTextureBase = "";
        private int textureIndex = 1;

        private Dictionary<Texture2D, string> textures = new Dictionary<Texture2D, string>();

        /// <summary>
        /// Constructor - sets the model we are exporting
        /// </summary>
        /// <param name="viewerModel">Model we are exporting</param>
        public ModelExporter(ViewerModel viewerModel)
        {
            this.viewerModel = viewerModel;
            this.model = viewerModel.Model;
        }

        /// <summary>
        /// Export the model to a given filename
        /// </summary>
        /// <param name="fileName"></param>
        public bool Export(string fileName, XmodlOptions options)
        {
            this.fileName = fileName;
            this.filePath = Path.GetDirectoryName(fileName) + "\\";
            this.fileTextureBase = Path.GetFileNameWithoutExtension(fileName) + "-";
            textureIndex = 1;
            textures.Clear();

            try
            {
                FileStream stream = new FileStream(fileName, FileMode.Create);
                XmlTextWriter xml = new XmlTextWriter(stream, System.Text.Encoding.UTF8);

                xml.Formatting = Formatting.Indented;
                xml.WriteProcessingInstruction("xml", "version='1.0' encoding='UTF-8'");
                xml.WriteStartElement("model");

                // xmodl format version
                xml.WriteAttributeString("version", Version);
                if (options.CompactFormat)
                {
                    xml.WriteAttributeString("compact", "true");
                }

                WriteBones(xml, options.CompactFormat);

                // Write the effects
                WriteEffects(xml);

                // Write the vertex buffers
                WriteVertices(xml, options.CompactFormat);

                // Write the index buffer
                WriteIndices(xml, options.CompactFormat);

                // Write the meshes
                WriteMeshes(xml);

                xml.WriteEndElement();        // </cell>
                xml.Close();
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString(), "Error writing exported model file.",
                    MessageBoxButtons.OK, MessageBoxIcon.Error);

                return false;
            }


            return true;
        }

        /// <summary>
        /// Output the model bones
        /// </summary>
        /// <param name="xml"></param>
        private void WriteBones(XmlWriter xml, bool compact)
        {
            xml.WriteStartElement("bones");
            if (model.Root != null)
                xml.WriteAttributeString("root-bone", model.Root.Index.ToString());

            foreach (ModelBone bone in model.Bones)
            {
                xml.WriteStartElement("bone");

                if (compact)
                {
                    // We omit the index in compact mode
                    xml.WriteAttributeString("n", CleanName(bone.Name));
                    if (bone.Parent != null)
                        xml.WriteAttributeString("p", bone.Parent.Index.ToString());
                    WriteAttributeMatrix(xml, "t", bone.Transform);
                }
                else
                {
                    xml.WriteAttributeString("index", bone.Index.ToString());
                    xml.WriteAttributeString("name", CleanName(bone.Name));
                    if (bone.Parent != null)
                        xml.WriteAttributeString("parent", bone.Parent.Index.ToString());
                    WriteAttributeMatrix(xml, "transform", bone.Transform);
                }

                xml.WriteEndElement();
            }

            if(viewerModel.ModelExtra != null) 
            {
                List<int> skeleton = viewerModel.ModelExtra.Skeleton;
                if (skeleton.Count > 0)
                {
                    xml.WriteStartElement("skeleton");

                    StringBuilder str = new StringBuilder();
                    foreach (int s in skeleton)
                    {
                        if (str.Length > 0)
                        {
                            str.Append(" ");
                        }

                        str.Append(s);
                    }

                    xml.WriteString(str.ToString());

                    xml.WriteEndElement();
                }
            }

            xml.WriteEndElement();  // </bones>
        }

        private Vector3 specularOther = Vector3.Zero;
        private Vector3 transmission = Vector3.Zero;
        private string pattern = @"([st]\([0-9]?\.[0-9]+, *[0-9]?\.[0-9]+, *[0-9]?\.[0-9]+ *\)|e\([0-9]?\.[0-9]+\))";

        /// <summary>
        /// Write the meshes to the XML file
        /// </summary>
        /// <param name="xml"></param>
        private void WriteMeshes(XmlWriter xml)
        {
            xml.WriteStartElement("meshes");

            foreach (ModelMesh mesh in model.Meshes)
            {
                // Camera.Position is special and should not show up in the output
                if (mesh.Name == "Camera.Position")
                    continue;

                xml.WriteStartElement("mesh");

                // 
                // Process special information that may be contained in the name
                //
                
                MatchCollection matches = Regex.Matches(mesh.Name, pattern);
                if (matches.Count > 0)
                {
                    foreach (Match m in matches)
                    {
                        MatchCollection matches2 = Regex.Matches(m.Value, @"([0-9]?\.[0-9]+)[,\) ]");
                        if (matches2.Count >= 3)
                        {
                            char t = m.Value[0];
                            switch (t)
                            {
                                case 's':
                                    specularOther.X = float.Parse(matches2[0].Groups[1].Value);
                                    specularOther.Y = float.Parse(matches2[1].Groups[1].Value);
                                    specularOther.Z = float.Parse(matches2[2].Groups[1].Value);
                                    break;

                                case 't':
                                    transmission.X = float.Parse(matches2[0].Groups[1].Value);
                                    transmission.Y = float.Parse(matches2[1].Groups[1].Value);
                                    transmission.Z = float.Parse(matches2[2].Groups[1].Value);
                                    break;
                            }

                        }
                        else if (matches2.Count == 1)
                        {
                            
                        }
                    }

                    xml.WriteAttributeString("name", CleanName(mesh.Name));
                }
                else
                {
                    specularOther = Vector3.Zero;
                    transmission = Vector3.Zero;
                    xml.WriteAttributeString("name", mesh.Name);
                }

                xml.WriteAttributeString("bone", mesh.ParentBone.Index.ToString());

                string bs = mesh.BoundingSphere.Center.X.ToString() + " " +
                    mesh.BoundingSphere.Center.Y.ToString() + " " +
                    mesh.BoundingSphere.Center.Z.ToString() + " " +
                    mesh.BoundingSphere.Radius.ToString();

                xml.WriteAttributeString("bounding-sphere", bs);

                // Write the mesh parts
                WriteMeshParts(xml, mesh);

                xml.WriteEndElement();
            }

            xml.WriteEndElement();
        }

        /// <summary>
        /// Remove any extra information from a name
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        private string CleanName(string name)
        {
            name = Regex.Replace(name, pattern, "");
            return name.Trim();
        }

        /// <summary>
        /// Lookup table that maps an effect path to an index into 
        /// the list of effects
        /// </summary>
        private Dictionary<Effect, int> effectToIndex = new Dictionary<Effect, int>();


        /// <summary>
        /// Write all of the effects found in the entire model, keeping
        /// track of them in the dictionary effectToIndex so we can 
        /// reference them in the mesh parts.
        /// </summary>
        /// <param name="xml"></param>
        private void WriteEffects(XmlWriter xml)
        {
            effectToIndex.Clear();

            xml.WriteStartElement("effects");

            int ndx = 0;
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (Effect effect in mesh.Effects)
                {
                    // Is this effect already there?
                    if (effectToIndex.ContainsKey(effect))
                        continue;

                    // Save so we can look it up later
                    effectToIndex[effect] = ndx;

                    xml.WriteStartElement("effect");
                    xml.WriteAttributeString("name", viewerModel.GetEffectName(effect));

                    if (effect is BasicEffect)
                    {
                        BasicEffect beffect = effect as BasicEffect;
                        float xshininess = beffect.SpecularPower;
                        xml.WriteAttributeString("alpha", beffect.Alpha.ToString());
                        WriteAttributeVector(xml, "diffuse", beffect.DiffuseColor);
                        WriteAttributeVector(xml, "emissive", beffect.EmissiveColor);
                        WriteAttributeVector(xml, "specular", beffect.SpecularColor);
                        xml.WriteAttributeString("shininess", xshininess.ToString());

                        if (specularOther.LengthSquared() > 0)
                        {
                            WriteAttributeVector(xml, "specularOther", specularOther);
                        }

                        if (transmission.LengthSquared() > 0)
                        {
                            WriteAttributeVector(xml, "transmission", transmission);
                        }

                        WriteTexture(xml, beffect.Texture);
                    }
                    else if(effect is SkinnedEffect) 
                    {
                        SkinnedEffect seffect = effect as SkinnedEffect;
                        float xshininess = seffect.SpecularPower;
                        xml.WriteAttributeString("alpha", seffect.Alpha.ToString());
                        WriteAttributeVector(xml, "diffuse", seffect.DiffuseColor);
                        WriteAttributeVector(xml, "emissive", seffect.EmissiveColor);
                        WriteAttributeVector(xml, "specular", seffect.SpecularColor);
                        xml.WriteAttributeString("shininess", xshininess.ToString());
                        xml.WriteAttributeString("skinned", "true");

                        if (specularOther.LengthSquared() > 0)
                        {
                            WriteAttributeVector(xml, "specularOther", specularOther);
                        }

                        if (transmission.LengthSquared() > 0)
                        {
                            WriteAttributeVector(xml, "transmission", transmission);
                        }

                        WriteTexture(xml, seffect.Texture);
                    }

                    xml.WriteEndElement();

                    ndx++;
                }

            }

            xml.WriteEndElement();
        }

        private void WriteTexture(XmlWriter xml, Texture2D texture)
        {
            if(texture == null) 
            {
                return;
            }

            string tFileName = "";
            if (texture != null)
            {
                if (!textures.ContainsKey(texture))
                {
                    // First time we have seen this texture
                    // Create it as a file.
                    tFileName = fileTextureBase + textureIndex.ToString() + ".png";
                    textureIndex++;
                    textures[texture] = tFileName;

                    using (Stream stream = File.OpenWrite(filePath + tFileName))
                    {
                        texture.SaveAsPng(stream, texture.Width, texture.Height);
                    }
                }
                else
                {
                    tFileName = textures[texture];
                }

                xml.WriteAttributeString("texture", tFileName);
            }
        }


        /// <summary>
        /// Lookup table that maps a vertex buffer into an index into the list
        /// of vertex buffers
        /// </summary>
        private Dictionary<VertexBuffer, int> verticesToIndex = new Dictionary<VertexBuffer, int>();

        /// <summary>
        /// Write the vertex buffers associated with this mesh. These are now 
        /// actually associated with the mesh parts, but they can be duplicated, so
        /// this collects them up and indexes them.
        /// </summary>
        /// <param name="xml"></param>
        private void WriteVertices(XmlWriter xml, bool compact)
        {
            verticesToIndex.Clear();

            xml.WriteStartElement("vertexbuffers");

            int ndx = 0;
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (ModelMeshPart meshpart in mesh.MeshParts)
                {
                    VertexBuffer vbuffer = meshpart.VertexBuffer;

                    // Have we already seen the vertex buffer associated with
                    // this mesh part?
                    if (verticesToIndex.ContainsKey(meshpart.VertexBuffer))
                        continue;

                    // Save so we can look it up later
                    verticesToIndex[vbuffer] = ndx;

                    xml.WriteStartElement("vertices");

                    VertexDeclaration vdecl = vbuffer.VertexDeclaration;

                    int strideBytes = vdecl.VertexStride;
                    int stride = strideBytes / sizeof(float);
                    int numvertices = vbuffer.VertexCount;

                    xml.WriteAttributeString("cnt", numvertices.ToString());

                    // Determine offset to the vertex, normal, and texture coordinate
                    int normalOffset = -1;
                    int tcoordOffset = -1;
                    int blendIndices = -1;
                    int blendWeights = -1;
                    VertexElement[] elements = vdecl.GetVertexElements();
                    foreach (VertexElement element in elements)
                    {
                        VertexElementFormat format = element.VertexElementFormat;
                        
                        switch (element.VertexElementUsage)
                        {

                            case VertexElementUsage.Normal:
                                normalOffset = element.Offset / sizeof(float);
                                break;

                            case VertexElementUsage.TextureCoordinate:
                                if(element.UsageIndex == 0)
                                    tcoordOffset = element.Offset / sizeof(float);
                                break;

                            case VertexElementUsage.BlendIndices:
                                blendIndices = element.Offset;
                                break;

                            case VertexElementUsage.BlendWeight:
                                blendWeights = element.Offset / sizeof(float);
                                break;
                        }

                    }

                    // Get the vertex data
                    float[] vertices = new float[numvertices * stride];
                    vbuffer.GetData(vertices);

                    byte[] vbytes = new byte[numvertices * strideBytes];
                    vbuffer.GetData(vbytes);

                    if (compact)
                    {
                        // Compact version.
                        // One tag for each type, binary data
                        WriteCompactVertexData(xml, "vertices-compact", vertices, numvertices, stride, 0, 3);
                        
                        if(normalOffset >= 0)
                            WriteCompactVertexData(xml, "normals-compact", vertices, numvertices, stride, normalOffset, 3);

                        if(tcoordOffset >= 0)
                            WriteCompactVertexData(xml, "tcoords-compact", vertices, numvertices, stride, tcoordOffset, 2);

                        if(blendWeights >= 0)
                            WriteCompactVertexData(xml, "weights-compact", vertices, numvertices, stride, blendWeights, 4);

                        if(blendIndices >= 0)
                            WriteCompactBlendIndices(xml, "blends-compact", vbytes, numvertices, strideBytes, blendIndices);

                    }
                    else
                    {
                        // Non-compact version
                        // One v tag for each vertex
                        for (int i = 0; i < numvertices; i++)
                        {
                            int o = i * stride;
                            int ob = i * strideBytes;

                            xml.WriteStartElement("v");

                            // Write the vertex
                            xml.WriteAttributeString("v", vertices[o] + " " + vertices[o + 1] + " " + vertices[o + 2]);

                            if (normalOffset >= 0)
                            {
                                // Write the normal
                                int n = o + normalOffset;
                                xml.WriteAttributeString("n", vertices[n] + " " + vertices[n + 1] + " " + vertices[n + 2]);
                            }

                            if (tcoordOffset >= 0)
                            {
                                int t = o + tcoordOffset;
                                xml.WriteAttributeString("t", vertices[t] + " " + vertices[t + 1]);
                            }

                            if (blendIndices >= 0)
                            {
                                int t = ob + blendIndices;
                                xml.WriteAttributeString("b", vbytes[t] + " " + vbytes[t + 1] + " " + vbytes[t + 2] + " " + vbytes[t + 3]);
                            }

                            if (blendWeights >= 0)
                            {
                                // Write the normal
                                int n = o + blendWeights;
                                xml.WriteAttributeString("w", vertices[n] + " " + vertices[n + 1] + " " + vertices[n + 2] + " " + vertices[n + 3]);
                            }

                            xml.WriteEndElement();
                        }
                    }



                    xml.WriteEndElement();
                    ndx++;
                }
            }
            
            xml.WriteEndElement();
        }

        /// <summary>
        /// Write vertex buffer data in a compact format. Only for float values
        /// </summary>
        /// <param name="xml">XML writer</param>
        /// <param name="tag">Name of the tag we create</param>
        /// <param name="vertices">Array of all vertex buffer data</param>
        /// <param name="numvertices">How many vertices there are</param>
        /// <param name="stride">Number of floats per vertex</param>
        /// <param name="offset">Offset into the vertex buffer for the part of the vertex</param>
        /// <param name="cnt">Number of floats for the component</param>
        private void WriteCompactVertexData(XmlWriter xml, string tag, float[] vertices, int numvertices, int stride, int offset, int cnt)
        {
            xml.WriteStartElement(tag);
            xml.WriteAttributeString("endian", BitConverter.IsLittleEndian ? "little" : "big");

            // Each vertex is cnt floats at 4 bytes each
            byte[] buffer = new byte[numvertices * cnt * 4];
            int bndx = 0;

            for (int i = 0; i < numvertices; i++)
            {
                int o = i * stride + offset;

                for (int k = 0; k < cnt; k++)
                {
                    byte[] f = BitConverter.GetBytes(vertices[o + k]);
                    for (int j = 0; j < 4; j++)
                    {
                        buffer[bndx++] = f[j];
                    }
                }
            }

            xml.WriteBase64(buffer, 0, buffer.Length);

            xml.WriteEndElement();
        }

        private void WriteCompactBlendIndices(XmlWriter xml, string tag, byte[] vertices, int numvertices, int stride, int offset)
        {
            xml.WriteStartElement(tag);

            // Each vertex is 4 weights as bytes
            StringBuilder str = new StringBuilder();

            for (int i = 0; i < numvertices; i++)
            {
                int o = i * stride + offset;

                for (int k = 0; k < 4; k++)
                {
                    // Convert to a character starting at 0
                    char ch = (char)(vertices[o + k] + 48);

                    // If we get the < or > character, substitute ( and ). That is
                    // because XML will escape < and >, so this makes the result smaller
                    if (ch == '<')
                        ch = '(';
                    else if (ch == '>')
                        ch = ')';

                    str.Append(ch);
                }
            }

            xml.WriteString(str.ToString());

            xml.WriteEndElement();
        }


        /// <summary>
        /// Lookup table that maps an index buffer into an index into the list
        /// of index buffers
        /// </summary>
        private Dictionary<IndexBuffer, int> indicesToIndex = new Dictionary<IndexBuffer, int>();

        private void WriteIndices(XmlWriter xml, bool compact)
        {
          //  indicesToIndex.Clear();

            xml.WriteStartElement("indexbuffers");

            int ndx = 0;
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (ModelMeshPart meshpart in mesh.MeshParts)
                {
                    IndexBuffer ibuffer = meshpart.IndexBuffer;

                    // Have we already seen the vertex buffer associated with
                    // this mesh part?
                    if (indicesToIndex.ContainsKey(ibuffer))
                        continue;

                    // Save so we can look it up later
                    indicesToIndex[ibuffer] = ndx;

                    xml.WriteStartElement(compact ? "indices-compact" : "indices");

                    // Indices can be supplied as 16 bit or 32 bit values
                    int elementSize = ibuffer.IndexElementSize == IndexElementSize.SixteenBits ? 2 : 4;
                    int numElements = ibuffer.IndexCount;
                    xml.WriteAttributeString("cnt", numElements.ToString());
                    xml.WriteAttributeString("size", elementSize.ToString());

                    if (compact)
                    {
                        // Move the data into a BYTE buffer
                        byte [] buffer = new byte[numElements * elementSize];
                        int n = 0;
                        if (elementSize == 2)
                        {
                            short[] indices = new short[numElements];
                            ibuffer.GetData<short>(indices);

                            for (int i = 0; i < numElements; i++)
                            {
                                short val = indices[i];
                                buffer[n++] = (byte)(val & 0xff);
                                val >>= 8;
                                buffer[n++] = (byte)(val & 0xff);
                            }
                        }
                        else
                        {
                            int[] indices = new int[numElements];
                            ibuffer.GetData<int>(indices);

                            for (int i = 0; i < numElements; i++)
                            {
                                int val = indices[i];
                                buffer[n++] = (byte)(val & 0xff);
                                val >>= 8;
                                buffer[n++] = (byte)(val & 0xff);
                                val >>= 8;
                                buffer[n++] = (byte)(val & 0xff);
                                val >>= 8;
                                buffer[n++] = (byte)(val & 0xff);
                            }
                        }

                        xml.WriteBase64(buffer, 0, buffer.Length);
                    }
                    else
                    {
                        StringBuilder sb = new StringBuilder();
                        if (elementSize == 2)
                        {
                            short[] indices = new short[numElements];
                            ibuffer.GetData<short>(indices);

                            for (int i = 0; i < numElements; i++)
                            {
                                sb.Append(indices[i].ToString());
                                sb.Append(" ");
                            }
                        }
                        else
                        {
                            int[] indices = new int[numElements];
                            ibuffer.GetData<int>(indices);

                            for (int i = 0; i < numElements; i++)
                            {
                                sb.Append(indices[i].ToString());
                                sb.Append(" ");
                            }
                        }

                        xml.WriteAttributeString("i", sb.ToString());
                    }

                    xml.WriteEndElement();
                    ndx++;
                }
            }

            xml.WriteEndElement();
        }

        private void WriteMeshParts(XmlWriter xml, ModelMesh mesh)
        {
            xml.WriteStartElement("parts");

            foreach (ModelMeshPart meshPart in mesh.MeshParts)
            {
                // Each part is a triangle list in the indices
                xml.WriteStartElement("part");

                xml.WriteAttributeString("base-vertex", meshPart.VertexOffset.ToString());
                xml.WriteAttributeString("num-vertices", meshPart.NumVertices.ToString());
                xml.WriteAttributeString("num-triangles", meshPart.PrimitiveCount.ToString());
                xml.WriteAttributeString("start-index", meshPart.StartIndex.ToString());
                xml.WriteAttributeString("vertices", verticesToIndex[meshPart.VertexBuffer].ToString());
                xml.WriteAttributeString("indices", indicesToIndex[meshPart.IndexBuffer].ToString());
                xml.WriteAttributeString("effect", effectToIndex[meshPart.Effect].ToString());

                xml.WriteEndElement();
            }

            xml.WriteEndElement();
        }

        private void WriteAttributeMatrix(XmlWriter xml, string name, Matrix matrix)
        {
            // Note:  Writes it column order - matches normal matrix order
            string attr = matrix.M11.ToString() + " " +
                matrix.M21.ToString() + " " +
                matrix.M31.ToString() + " " +
                matrix.M41.ToString() + " " +
                matrix.M12.ToString() + " " +
                matrix.M22.ToString() + " " +
                matrix.M32.ToString() + " " +
                matrix.M42.ToString() + " " +
                matrix.M13.ToString() + " " +
                matrix.M23.ToString() + " " +
                matrix.M33.ToString() + " " +
                matrix.M43.ToString() + " " +
                matrix.M14.ToString() + " " +
                matrix.M24.ToString() + " " +
                matrix.M34.ToString() + " " +
                matrix.M44.ToString();

            xml.WriteAttributeString(name, attr);
        }

        private void WriteAttributeVector(XmlWriter xml, string name, Vector3 vector)
        {
            xml.WriteAttributeString(name, vector.X.ToString() + " " + vector.Y.ToString() + " " + vector.Z.ToString());
        }
    }
}
